This document bases on information and testing done
with IIS 1.0. We have not re-tried it with later versions. However, we feel very comfortable with the
information contained herein and think that it still is correct.
There is just one exception: if you create an
application in MMC under IIS 4.0, your ISAPI extension is
run in a separate process space (as far as we know under
MTS control). In this case, you obviously do not run in
the process space of IIS (that's why it is called process
isolation). As far as your extension is concerned, that
should make no big difference.
Any additions to this document are welcome at RGerhards@Adiscon.com.
Introduction
When we first started developing isapi apps, things
were first very clear to us. However, as soon as we hit
the field of "real" IIS application with
multiple concurrent requests and multiple extensions
being loaded, things became quite more complicated. All
of a sudden, some things looked really confusing.
We found that the key to order this things at get a
productive application out of our coding is understanding
what IIS really does and how an extension is executed.
This is what we call the ISAPI Execution Environmen and
this is what this document is all about.
We hope it will save you some valuable time when first
starting isapi development.
In order to allow easy offline storage and printout,
we have created just this single, relativly large
document. People with slow links and/or not-so-current
browsers, please forgive us. We think it is the best
compromise available.
If you find anything to add, correct or otherwise
change in this document, please do not hesitate to mail us. Any comments
are welcome.
Overview
If you start developing server extensions, you need to
know something about their execution environment. ISAPI
programs - extensions as well as filters - are a very
special kind of animal. Being DLLs, they are no processes
by themself - rather they share a common process space
(IIS' process). Thus they possibly interact with both the
IIS as well as other ISAPI apps. Furthermore, IIS can
handle multiple requests concurrently. Thus your isapi
app can be called by more than one thread concurrently.
ISAPI is a very, very performant interface for highly
sophisticated interactive web applications. However, such
a highly performant interface has its cost. This - in our
turn - is the additional care to be taken developing your
applications. Please don't feel scared away from isapi:
it can be easily used if you know about its special
nature and take care for its special needs. And this is
all this document is about.
Life of an ISAPI App
An isapi app has a very special life cycle. It is
dependent on its host, the IIS. First, lets look what IIS
does during its life and how this interferes with our
app:
- First of all, its not the web publishing service
that is loaded by the os' service control
manager. Rather it is a program called
"inetinfo.exe" which in turn starts the
"real services" (implemented as DLLs).
We in turn will focus on the Web Publishing
Service, which is named "w3svc".
- When w3svc starts, it does its initialization
work. During its initialization, it checks the
registry for installed isapi filters and
initializes them (note: the life of an isapi
filter won't be discussed any further). W3svc
also loads and initializes some other WOSA
services, like Windows sockets and ODBC (for
logging purposes).
- During its initialization phase IIS creates a
number of threads that later on will be used to
serve clients. If you look with the process
viewer application at an just started-up IIS,
there are usually between 12 and 14 threads in
its process space, a lot of them without any
processing so far. We think these threads build a
pool of "worker threads". However, we
found no documentation on that so far.
- Being started up, IIS listens for HTTP-requests.
As soon as one comes in, IIS assigns a worker
thread (usually the same that's the shortest one
finished) to the request and carries on
processing (we think - but have not verified -
that the number of worker threads is extensible
as more concurrent requests come in).
- The worker thread then analyses the type of request. It uses the configuration settings
supplied by Internet Service Manager to decide on
what to do next. Choices are "web file
services" (just deliver the need file more
or less unprocessed) or call an extension. In the
later case there can be numerous extensions to
call - basically they are all the same: call an
isapi extension. This is even true with IDC, in
which case the IDCODBC.DLL isapi extension is called.
- Now we get into business. Let's think the worker
thread has requested that our app is to call.
- If not already loaded, IIS then first
loads our DLL. As usual, this triggers
the execution of our DllMain entry point.
Here is the right place to do any global
initialization of our extension. There
is nothing IIS specific at this stage of
execution!
- Soon after loading the DLL, IIS requests
our extension to register with IIS. This
is done by calling our routine
GetExtensionVersion. Here we have to
supply a little information about ourselvs: like our name and the isapi
version we are developed for. Please note
that GetExtensionVersion is only called once.
It won't get called further on, not
because we are executed by another worker
thread or for whatever other reason. The
only case in which an isapi extension's
GetExtensionVersion will be called again
is after unloading and reloading the DLL
(as the whole process starts from
scratch). For now we can remind:
GetExtensionVersion will only once be
called in the lifecycle of our app.
- Now IIS is in business with us. It knows
how to handle us (depending on isapi
version) and now starts activating us.
For any further http request, IIS starts
at this very point, skipping all
extension initialization work.
- Now our main entry point -
HttpExtensionProc- is called by the
worker thread. IIS supplies us with the
input data stream and allows us to write
to the client. Please note the multiple
concurrent http requests for our
application result in multiple worker
threads calling our app concurrently. So
at any given time, our code might be
executed by numerous worker threads
simultaneously. As soon as our processing
is done (HttpExtensionProc returns), IIS
begins to finish processing. Our
DLL remains loaded.
- The worker thread now does everything need to
complete the request. After that, it is available
for usage by another request and put on hold
until such a requests reactivates it. We do not
know if worker threads will be reduced when IIS'
workload is reduced. However, we do expect it
(another nice thing worth tracing).
- More important than that is what happened to our
DLL: This one is still sitting in IIS' process
space waiting for any other requests hitting it.
It has not been unloaded, resulting in possible
very low overhead for each additional call (this
is one of the performance strength of isapi).
- However, the server might decide to remove our
DLL at any given time. Again, we do not know the
details. But it might happen. In this case, IIS
calls UnloadLibrary and thus our DllMain
procedure is hit again. Now is the right time to
clean up any resources globally allocated (free
any handles, heap memory structures etc.). After
that, our extension is in an uninitialized
(unregistered to IIS) state. If we get another
request, the whole initialization process starts
again (thus IIS tries to avoid unloading
extensions).
- One thing missing in IIS lifecycle: if the user
decides to shut down IIS (or the NT OS at large),
IIS unloads all still-loaded DLLs (as might be
our extension), closes down all worker threads
and finally shuts down itself.
This is what IIS does during its life. At least this
is what we
think it does. This is a good place to remind
you that we are neither Microsoft nor have written IIS
nor have access to its source. All the above is a)
extracted from documentation, b) seen in the debugger and
system tools, c) experienced and - very important - d) put
together based on general software engineering principles.
IIS' developers might have done things quite different
and we can be in error.
However, there is also good news: at least the
understanding of the above written has helped us pretty
well when developing our extensions. So it might not be
correct, it still should be helpful.
Well, that should be enough of warnings. Back to
software again. If you look at the above scenario, there
are a number of important points in it:
- If you have to deal with global structures inside
your extension, DllMain is a perfect place to
deal with them.
- Do not lock any resources between requests. You
never know when you will be called next: whether
in the next microsecond or in five weeks time.
Thus, no ressources should be occupied by your
"ever active" app.
- Be sure to handle multithreading correctly (also
see section below on this topic)
- Never expect to be called by the same worker
thread again. It might happen, it might not.
- Clean up after each request! Other extensions
might be called by the thread that just executed
your coding.
If you follow this guidelines, you can be pretty sure
not to harm anything in IIS.
Things to watch for
Memory Protection
Being part of IIS process space, isapi apps have full
access to all of this space. Thus it is not only possible
to read data at any location of the process space, your
app can also write at any writable memory
location. This includes locations outside of
your isapi app, including some that are vital for IIS'
health. An isapi app can thus easily crash IIS and stop
it from running. Keep this in mind when debugging you
applications. They deserve very special attention, as
your application's error can cause some other
applications - possibly some highly important ones - to
crash instantly (well, looks a bit like being back in
good old Win16 days...).
Multithreading
Isapi apps get called by multiple IIS threads
concurrently (given concurrent HTTP requests). Thus your
coding needs to be threadsafe. You have to apply special
threadsafe coding technologies in order to get things
running well.
In contrast to some of your other multithreaded
applications, however, you do not have
control over the "main" thread of your program.
First let's explain what I mean with main thread. This is
the thread that initially gets control, initializes
global data structures and the
"multithreading". With multithreading
initialization I mean the creation of synchronization
objects and subordinate threads (as well as, of course,
all data that is used by your application to control
useful multithreading work).
One example of this might be the allocation of an ODBC
environment handle. According to ODBC SDK documentation
such a handle should exist only once in a process space.
The resulting handle itself is threadsafe and can be used
by multiple threads concurrently. Thus, in your regular
application, you might create the environment handle
inside your main thread at startup, than go to your
regular work (which involves the multithreading) and - at
process termination time - close the handle. You'll never
run into trouble, as your application has total control
over the usage of its handle and who's using it.
With IIS, however, things are quite different. Here
the main thread is IIS. IIS unfortunately doesn't know
about your need of an ODBC environment handle. Thus its
main thread doesn't allocate one (at least we don't know
about this) and in turn can not pass it down to you. So
your only option is to allocate the handle at DLL startup
(using DllMain) and free it at unload time. This works
pretty well as long as there is only one
isapi app doing ODBC operations. But what about a second
one concurrently being loaded? Doesn't this one also need
to get and free a handle? Sure it does. And - voila -
here we have two handles inside one process space. As far
as I know, you have absolutely no chance to get around
this (as you do not control the main thread).
Giving this example, there is one question: why do
real-life IIS installations not crash? To be honest - I
don't know. Most isapi apps do exactly what I have
described. Without ever crashing. I have run quite a lot
of tests with a large number of concurrently allocated
ODBC environment handles - and it never crashed. To me,
it looks as if only documentation is in error and this
operations seems to be pretty OK.
The above example is an extreme side of IIS'
multithreading topic: it's not really all that bad, but
be warned about what you are doing and what implications
it has. Please note that there may be other scenarios
like the "ODBC environment handle scenario"
described above. Also keep in mind that your perfectly
well coded isapi app might run into trouble if it meets a
not-so-well coded other app in the same server.
And if you have a look at just your application: be
sure to use only threadsafe technologies and coding. Be
very skeptic about third party software (MFC included).
Dig into the topic to be sure that it is really
threadsafe. Check it with multiple concurrent executions
at the beginning of your development project.
Inside your own coding, be although sure to code
threadsafe. You won't need special coding technologies
just to get your isapi app up and running. If there is no
need to access "global" objects inside your
app, you probably will never need any special coding.
However, if you use e.g. global data structures you
should protect them from improper usage by your threads.
If you need to do this, use synchronization structures
like critical sections. Make sure that no more than one
thread can access such a critical object at a given time.
By the way: this is they way to get
around with non-threadsafe third party coding: if you
*really* have to use it, put it in a critical section and
be happy (although this has some performance
implications). This is what Microsoft did in its OLEISAPI
sample coding (for NT w/o DCOM).
If you have special interest in IIS' multithreading
issues, you might want to download our IIS
Multithreading Demo (isapithrd.dll) program.
Isapithrd displays which worker thread is currently
executing it and how many instances are concurrently
executed.
Stack Size
Stack is nearly unlimited in Win32, isn't it? You'd
probably ask. Well, it is. Each of your threads might
address up to 1 MB stack space. Not unlimited but pretty
much. So there shouldn't be any problem, should it?
Well, there is one little tiny thing to take into
consideration. Whenever a thread (the main thread for
example) creates a new thread, it specifies the new
threads maximum stack size. Fine if it is the 1 MB
maximum. Less nice, if it is below. As far as we know,
IIS does limit an isapi apps stack size.
We guess that is in expectation of many, many apps being
loaded concurrently and sharing the 2 GB process
address space (but even then there would be plenty of
virtual space for their stacks....).
We do not know the exact reason for this limit.
However, due to many reports on stack overflow, we do
know there is a limit! We have not yet tried to get the
exact number. But as a general guideline, we recommend
allocating as few stack space as possible. If you have
larger size data structures, it's preferably to use the
heap (using the "new" operator). However, if
you use the heap, keep in mind that you should free any
allocated memory!
MFC classes
We (and others as well) had some bad experiences with
MFC classes (version 4.x!). Before you use any class, be
sure to check if it can successfully be used inside an
isapi app. We know that at least the following is
dangerous:
With the release of VC++ 5.0 this should be history.
Be sure to upgrade to this version to avoid any MFC
problems inside your ISAPI app.
Disclaimer
The information contained in this document is to the
best of our knowledge. However, we do not guarantee its
accuracy. Use the information contained herein at your
sole risk.
|